package adc

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"fmt"
	"github.com/speps/go-hashids/v2"
	"go.uber.org/zap"
	"golang.org/x/crypto/pbkdf2"
	"strings"
	"sync"
	"time"
	"wzgames/internal/admin/adcomm"
	"wzgames/internal/admin/admindao"
	"wzgames/internal/admin/api/wzabind"
	"wzgames/internal/admin/model"
	"wzgames/internal/client"
	"wzgames/internal/constant"
	"wzgames/internal/gloader"
	"wzgames/internal/model/mcomm"
	"wzgames/internal/response"
	"wzgames/pkg/components/mysql"
	"wzgames/pkg/components/redis"
	"wzgames/pkg/logger"
	"wzgames/pkg/utils/byteutil"
	"wzgames/pkg/utils/dateutil"
	"wzgames/pkg/utils/stringutil"
)

var (
	passport *Passport
	ponce    = sync.Once{}
)

const (
	defaultTimeout = time.Second * 5
)

type Passport struct {
	hashids *hashids.HashID

	adminDs *mysql.DS
	redisC  *redis.RedisClient
}

func PP() *Passport {
	return passport
}

func Init() error {
	var err error
	if passport == nil {
		ponce.Do(func() {
			passport = &Passport{}
			hd := hashids.NewData()
			hd.Salt = adcomm.AdminTicketSalt
			hd.MinLength = 32
			h, e := hashids.NewWithData(hd)
			if e != nil {
				logger.Error("[ADC] init admin hash ids instance fail", zap.Error(e))
				err = e
			} else {
				passport.hashids = h
			}
			passport.adminDs = gloader.CommMgr().GetDBProxy(constant.MysqlOfAdmin)
			passport.redisC = gloader.CommMgr().RedisPool()
		})
	}
	return err
}

func (pp *Passport) VerifyPwd(postPwd, salt string, itc int, dbPwd string) bool {
	enc := pbkdf2.Key(byteutil.StringToSliceByte(postPwd), byteutil.StringToSliceByte(salt), itc, 32, sha256.New)
	encOut := fmt.Sprintf("%x", enc)
	if encOut == dbPwd {
		return true
	}
	return false
}

func (pp *Passport) GenPwd(pwd, salt string, itc int) string {
	enc := pbkdf2.Key(byteutil.StringToSliceByte(pwd), byteutil.StringToSliceByte(salt), itc, 32, sha256.New)
	return fmt.Sprintf("%x", enc)
}

func (pp *Passport) GetAdmin(adminId int64) (*model.Admin, int16) {
	accDao := admindao.AdminDao{Driver: pp.adminDs.Ds()}
	adm, err := accDao.GetAdminById(adminId)
	if err != nil {
		if err != sql.ErrNoRows {
			logger.Error("[ADC] get admin Error on GetAdminById", zap.Int64("AdminId", adminId), zap.Error(err))
			return nil, response.ErrInternal
		}
		return nil, response.ErrNotFound
	}
	return adm, 0
}

func (pp *Passport) GetAdminByName(adminNane string) (*model.Admin, int16) {
	accDao := admindao.AdminDao{Driver: pp.adminDs.Ds()}
	adm, err := accDao.GetAdminByName(adminNane)
	if err != nil {
		if err != sql.ErrNoRows {
			logger.Error("[ADC] get admin Error on GetAdminByName", zap.String("AdminName", adminNane), zap.Error(err))
			return nil, response.ErrInternal
		}
		return nil, response.ErrNotFound
	}
	return adm, 0
}

func (pp *Passport) CheckAccount(req wzabind.SysLoginReq) (*model.Admin, int16) {
	accDao := admindao.AdminDao{Driver: pp.adminDs.Ds()}
	adm, err := accDao.GetAdminByName(req.AdminName)
	if err != nil {
		if err != sql.ErrNoRows {
			logger.Error("[ADC] check account Error on GetAdminByName", zap.String("AdminName", req.AdminName), zap.Error(err))
			return nil, response.ErrInternal
		}
		return nil, response.ErrNotFound
	}

	if adm.Status != mcomm.Status_Up {
		return nil, response.ErrAdminLocked
	}

	if pp.VerifyPwd(req.AdminPassword, adm.AdminSalt, adm.Itc, adm.AdminPwd) {
		return adm, 0
	}

	return nil, response.ErrUnAuthorize
}

func (pp *Passport) CreateAdmin(req wzabind.SysAddAdminReq) (*model.Admin, int16) {
	newId := client.GetID(601, 0)
	if newId == 0 {
		logger.Error("[ADC] create admin fail on gen adminId", zap.String("Name", req.AdminName))
		return nil, response.ErrInternal
	}

	accDao := admindao.AdminDao{Driver: pp.adminDs.Ds()}
	adm, err := accDao.GetAdminByName(req.AdminName)
	if err != nil {
		if err != sql.ErrNoRows {
			logger.Error("[ADC] create admin fail on gen GetAdminByName", zap.String("Name", req.AdminName))
			return nil, response.ErrInternal
		}
	} else if adm != nil && adm.AdminID > 0 {
		return nil, response.ErrAdminExists
	}
	salt := stringutil.RandomString(16)
	updated := dateutil.UTC()
	adm = &model.Admin{
		AdminID:   int64(newId),
		AdminName: req.AdminName,
		NickName:  req.NickName,
		Avatar:    req.Avatar,
		Status:    mcomm.Status_Up,
		AdminSalt: salt,
		AdminPwd:  pp.GenPwd(req.AdminPassword, salt, adcomm.PwdIter),
		Itc:       adcomm.PwdIter,
		Created:   updated,
		Updated:   updated,
	}

	err = accDao.CreateAdmin(adm)
	if err != nil {
		logger.Error("[ADC] create admin fail", zap.String("Name", req.AdminName), zap.Error(err))
		return nil, response.ErrInternal
	}

	return adm, 0
}

func (pp *Passport) genTicketKey(token string) string {
	var bd strings.Builder
	bd.WriteString(gloader.EnvString())
	bd.WriteString(adcomm.CacheAdminTokenPrefix)
	bd.WriteString(token)

	return bd.String()
}

func (pp *Passport) GenAdminTicket(adm *model.Admin, clientSessToken string) (string, int16) {
	token, err := pp.hashids.EncodeInt64([]int64{gloader.EnvHashAdjust(), adm.AdminID, dateutil.UnixMilli()})
	if err != nil {
		logger.Error("[ADC] gen admin ticket fail on hashids", zap.Int64("AdminId", adm.AdminID), zap.Error(err))
		return "", response.ErrInternal
	}

	key := pp.genTicketKey(token)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	_, err = pp.redisC.Get().HSet(ctx, key, adcomm.AdminIdKey, adm.AdminID, constant.CacheClientSessKey, clientSessToken).Result()
	if err == nil {
		_, err = pp.redisC.Get().Expire(ctx, key, dateutil.HalfHour*time.Second).Result()
	}
	cancel()
	if err != nil {
		logger.Error("[ADC] gen admin ticket fail on redis.Set", zap.Int64("AdminId", adm.AdminID), zap.Error(err))
		return "", response.ErrInternal
	}

	GetReceiver().StartAdmin(adm, token, clientSessToken)

	return token, 0
}

func (pp *Passport) CheckTicket(ipt string) (int64, int16) {
	arr := strings.Split(ipt, constant.Hyphen)
	if len(arr) != 2 || arr[0] == "" || arr[1] == "" {
		return 0, response.ErrUnAuthorize
	}
	token := arr[0]
	clientSessToken := arr[1]
	cacheKey := pp.genTicketKey(token)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	ret, err := pp.redisC.Get().HGetAll(ctx, cacheKey).Result()
	cancel()
	if err != nil {
		logger.Error("[ADC] check admin ticket fail on redis.HGetAll", zap.String("Token", token), zap.String("Client", clientSessToken), zap.Error(err))
		return 0, response.ErrInternal
	}
	if ret == nil {
		return 0, response.ErrUnAuthorize
	}
	var clientToken string
	if cToken, ok := ret[constant.CacheClientSessKey]; ok {
		clientToken = cToken
	}
	if clientToken != clientSessToken {
		return 0, response.ErrUnAuthorize
	}
	var adminIdStr string
	if v, ok := ret[adcomm.AdminIdKey]; ok {
		adminIdStr = v
	}
	adminId, _ := stringutil.ToInt64(adminIdStr, 0)
	if adminId == 0 {
		return 0, response.ErrUnAuthorize
	}
	ctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)
	pp.redisC.Get().Expire(ctx, cacheKey, dateutil.HalfDay*time.Second)
	cancel()

	return adminId, 0
}

func (pp *Passport) Logout(adminId int64, token string) {
	key := pp.genTicketKey(token)
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	_, err := pp.redisC.Get().Del(ctx, key).Result()
	cancel()
	if err != nil {
		logger.Error("[ADC] admin logout fail on redis.Del", zap.Int64("AdminId", adminId), zap.Error(err))
	}
}

func (pp *Passport) ChangeAdminPwd(admObj *model.Admin, newPwd string) int16 {
	accDao := admindao.AdminDao{Driver: pp.adminDs.Ds()}
	updated := dateutil.UTC()
	admObj.AdminSalt = stringutil.RandomString(16)
	admObj.Updated = updated
	admObj.AdminPwd = pp.GenPwd(newPwd, admObj.AdminSalt, admObj.Itc)
	_, err := accDao.ChangePwd(admObj)
	if err != nil {
		logger.Error("[ADC] modify admin pwd fail", zap.String("Name", admObj.AdminName), zap.Error(err))
		return response.ErrInternal
	}

	return 0
}

func (pp *Passport) ChangeAdminStatus(admObj *model.Admin, newStat mcomm.Status) int16 {
	accDao := admindao.AdminDao{Driver: pp.adminDs.Ds()}
	admObj.Updated = dateutil.UTC()
	admObj.Status = newStat
	_, err := accDao.ChangeStatus(admObj)
	if err != nil {
		logger.Error("[ADC] modify admin status fail", zap.String("Name", admObj.AdminName), zap.Error(err))
		return response.ErrInternal
	}

	return 0
}
